﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using Entangle.RequestAttributes;
using Entangle.RequestServiceReference;
using log4net;

namespace Entangle
{
    class Entangled
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(Entangled));

        #region Singleton
        private static Entangled _instance;
        private Entangled()
        {
            if (Configuration.SynchStatus == SynchStatus.Unknown)
                throw new EntanglementException("Cannot create Entangled instance before running successful synch");
            server = new ServerConnector(Configuration.SynchServer);
        }
        public static Entangled Instance
        {
            get
            {
                if (_instance == null)
                    _instance = new Entangled();

                return _instance;
            }
        }
        #endregion

        public IPEndPoint LastKnownPartnerAddress { get { return _lastKnownPartnerAddress; } }
        private IPEndPoint _lastKnownPartnerAddress;
        private RequestServiceClient client;

        private ServerConnector server;

        public EntanglementStatus Status { get { return _status; } }
        private EntanglementStatus _status = EntanglementStatus.Unknown;

        public void SetServer(string servAdd)
        {
            server = new ServerConnector(servAdd);
        }

        public PartnerState State
        {
            get { return _state; }
            set
            {
                _state = value;

                EventHandler<PartnerStateChangeEventArgs> temp = PartnerStateChange;
                if (temp != null)
                    temp(this, new PartnerStateChangeEventArgs(State));
            }
        }
        private PartnerState _state;

        private bool PartnerResponding { get { return true; } }
        public void Update()
        {
            if (!PartnerResponding)
                UpdateFromServer();
            client = new RequestServiceClient("BasicHttpBinding_IRequestService", String.Format(@"http://{0}:{1}/RequestService", Configuration.lastKnownPartnerIP, Properties.Settings.Default.PortNumber));
        }

        private void Check(MethodBase caller)
        {
            foreach (object a in caller.GetCustomAttributes(false))
            {
                if (a is RequiresFullSynch)
                {
                    if (Configuration.SynchStatus != SynchStatus.Synched)
                        throw new RequestException(String.Format("Request '{0}' requires full synch.", caller.Name), RequestException.INVALID_SYNCH_STATUS);
                }
            }
        }

        private Message RunRequest(Message message, MethodBase caller)
        {
            log.Info("Running request...");
            log.Debug("Message:" + message.ToString());
            bool tryDirect = true;

            foreach (object a in caller.GetCustomAttributes(false))
            {
                if (a is ServerOnly)
                    tryDirect = false;
            }

            if (tryDirect && Configuration.SynchStatus != SynchStatus.Synched)
            {
                tryDirect = false;
                log.Info("Overriding direct request because users are not fully synched yet.");
            }
            Message response;
            if (tryDirect){
                log.Info("Trying to reach partner directly.");
                try
                {
                    log.Info("Calling partner RequestService.SendRequestXML");
                    string sResponse = client.SendRequestXML(message.ToXMLString());
                    log.Info("Got response. Parsing into Message...");
                    response = Message.FromXML(sResponse);
                    log.Info("Response message created.");
                    return response;
                }
                catch (Exception e)
                {
                    log.Error("Error while invoking remote request service.", e);
                    log.Error("Will try server-side.");
                }
            }
            //not returned yet, so try server side:
            try
            {
                log.Info("Trying to contact server.");
                string result = server.Request(message.ToXMLString(), Configuration.UserID);
                log.Info("Got answer from server.");
                response = Message.FromServerString(result);
                log.Info("Returning server message.");
                return response;
            }
            catch (Exception e)
            {
                log.Error("Error while invoking server.", e);
                return null;
            }
        }

        #region PrivateRequests
        [PrivateRequest]
        private void LocatePartner()
        {
            Check(MethodBase.GetCurrentMethod());
        }

        [PrivateRequest]
        private void UpdateFromServer()
        {
            Check(MethodBase.GetCurrentMethod());
        }
        #endregion

        #region PublicRequests
        /// <summary>
        /// Requests partners public key from server. It passes plaintext userID, as nor that ID, nor public key are confidential information
        /// </summary>
        /// <returns>Message retrieved from server containing partners public key</returns>
        [ServerOnly]
        [PublicRequest]
        public Message RequestPublicKey()
        {
            Check(MethodBase.GetCurrentMethod());

            string payload = Configuration.UserID.ToString();
            Message m = new Message(payload, MessageType.PARTNER_PUBLIC_KEY);     
            return RunRequest(m, MethodBase.GetCurrentMethod());
        }

        [PublicRequest]
        [RequiresFullSynch]
        public Message AddAESKey(string keyXml)
        {
            Check(MethodBase.GetCurrentMethod());

            Message m = new Message(keyXml, MessageType.PARTNER_ADD_NEW_AES_KEY);
            return RunRequest(m, MethodBase.GetCurrentMethod());
        }

        [PublicRequest]
        [AsynchRequest]
        [RequiresFullSynch]
        public Message Notify(PartnerState stateToSend)
        {
            Check(MethodBase.GetCurrentMethod());

            XmlSerializer xs = new XmlSerializer(typeof(PartnerState));
            MemoryStream ms = new MemoryStream();
            XmlTextWriter tw = new XmlTextWriter(ms, Encoding.UTF8);
            xs.Serialize(tw, stateToSend);
            //ms = (MemoryStream)tw.BaseStream;
            string sXml = new UTF8Encoding().GetString(ms.ToArray());
            //string sXml = Convert.ToBase64String(ms.ToArray());
            Message m = new Message(sXml, MessageType.STATUS_CHANGED);
            return RunRequest(m, MethodBase.GetCurrentMethod());
        }
        #endregion

        #region PartnerState
        public event EventHandler<PartnerStateChangeEventArgs> PartnerStateChange;
        public void UpdateState(string stateXml, DateTime stateDate)
        {
            PartnerState state = new PartnerState();
            state.ArrivalTime = DateTime.Now;
            state.CreationTime = stateDate;

            XmlDocument xmld = new XmlDocument();
            xmld.LoadXml(stateXml);
            XmlElement xml = xmld.DocumentElement;

            XmlNode xnState = xml["State"];
            if (xnState == null)
                throw new XmlException("Missing requiered 'State' element.");
            string sState = xnState.InnerText;
            state.State = (PartnerStates)Enum.Parse(typeof(PartnerStates), sState);

            XmlNode xnTime = xml["Time"];
            if (xnTime == null)
                throw new XmlException("Missing requiered 'Time' element.");
            string sTime = xnTime.InnerText;
            state.Time = TimeSpan.FromMilliseconds(double.Parse(sTime));

            XmlNode xnMessage = xml["Message"];
            if (xnMessage == null)
                throw new XmlException("Missing requiered 'Message' element.");
            state.Message = xnMessage.InnerText;

            this.State = state;
        }
        #endregion
    }
}
